| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 |
- // @ts-nocheck
- import { PermissionAction } from '@supabase/shared-types/out/constants'
- import { IS_PLATFORM, useParams } from 'common'
- import { isEqual } from 'lodash'
- import { AlertCircle, CornerDownLeft, Loader2 } from 'lucide-react'
- import { useEffect, useMemo, useState } from 'react'
- import { toast } from 'sonner'
- import { LogoLoader } from 'ui'
- import { DeployEdgeFunctionWarningModal } from '@/components/interfaces/EdgeFunctions/DeployEdgeFunctionWarningModal'
- import { formatFunctionBodyToFiles } from '@/components/interfaces/EdgeFunctions/EdgeFunctions.utils'
- import { DefaultLayout } from '@/components/layouts/DefaultLayout'
- import EdgeFunctionDetailsLayout from '@/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout'
- import { PreventNavigationOnUnsavedChanges } from '@/components/ui-patterns/Dialogs/PreventNavigationOnUnsavedChanges'
- import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
- import { FileExplorerAndEditor } from '@/components/ui/FileExplorerAndEditor'
- import { FileData } from '@/components/ui/FileExplorerAndEditor/FileExplorerAndEditor.types'
- import { useEdgeFunctionBodyQuery } from '@/data/edge-functions/edge-function-body-query'
- import { useEdgeFunctionQuery } from '@/data/edge-functions/edge-function-query'
- import { useEdgeFunctionDeployMutation } from '@/data/edge-functions/edge-functions-deploy-mutation'
- import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
- import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
- import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
- import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
- import { BASE_PATH } from '@/lib/constants'
- const CodePage = () => {
- const { ref, functionSlug } = useParams()
- const { data: project } = useSelectedProjectQuery()
- const { data: org } = useSelectedOrganizationQuery()
- const { mutate: sendEvent } = useSendEventMutation()
- const [showDeployWarning, setShowDeployWarning] = useState(false)
- const { can: canDeployFunction } = useAsyncCheckPermissions(PermissionAction.FUNCTIONS_WRITE, '*')
- const { data: selectedFunction } = useEdgeFunctionQuery({
- projectRef: ref,
- slug: functionSlug,
- })
- const {
- data: functionBody,
- isPending: isLoadingFiles,
- isError: isErrorLoadingFiles,
- isSuccess: isSuccessLoadingFiles,
- error: filesError,
- } = useEdgeFunctionBodyQuery(
- {
- projectRef: ref,
- slug: functionSlug,
- },
- {
- // [Alaister]: These parameters prevent the function files
- // from being refetched when the user is editing the code
- retry: false,
- retryOnMount: false,
- refetchOnWindowFocus: false,
- staleTime: Infinity,
- refetchOnMount: false,
- refetchOnReconnect: false,
- refetchInterval: false,
- refetchIntervalInBackground: false,
- }
- )
- const [files, setFiles] = useState<FileData[]>([])
- const initialFiles = useMemo(() => {
- return !!functionBody
- ? formatFunctionBodyToFiles({
- functionBody,
- entrypointPath: selectedFunction?.entrypoint_path,
- })
- : []
- }, [functionBody, selectedFunction?.entrypoint_path])
- const { mutate: deployFunction, isPending: isDeploying } = useEdgeFunctionDeployMutation({
- onSuccess: () => {
- toast.success('Successfully updated edge function')
- setShowDeployWarning(false)
- setFiles((files) =>
- files.map((f) => {
- return { ...f, state: 'unchanged' }
- })
- )
- },
- })
- const fileExists = (filePath: string | undefined): boolean => {
- return filePath ? files.some((file) => file.name === filePath) : false
- }
- const onUpdate = async () => {
- if (isDeploying || !ref || !functionSlug || !selectedFunction || files.length === 0) return
- try {
- const entrypoint_path =
- functionBody?.metadata?.deno2_entrypoint_path ?? selectedFunction.entrypoint_path
- const newEntrypointPath = entrypoint_path?.split('/').pop()
- const newImportMapPath = selectedFunction.import_map_path?.split('/').pop()
- const entrypointExists = fileExists(newEntrypointPath)
- const importMapExists = fileExists(newImportMapPath)
- deployFunction({
- projectRef: ref,
- slug: selectedFunction.slug,
- metadata: {
- name: selectedFunction.name,
- verify_jwt: selectedFunction.verify_jwt,
- ...(entrypointExists && { entrypoint_path: newEntrypointPath }),
- ...(importMapExists && { import_map_path: newImportMapPath }),
- },
- files: files.map(({ name, content }) => ({ name, content })),
- })
- } catch (error) {
- toast.error(
- `Failed to update function: ${error instanceof Error ? error.message : 'Unknown error'}`
- )
- }
- }
- const handleDeployClick = () => {
- if (files.length === 0 || isLoadingFiles) return
- setShowDeployWarning(true)
- sendEvent({
- action: 'edge_function_deploy_updates_button_clicked',
- groups: {
- project: ref ?? 'Unknown',
- organization: org?.slug ?? 'Unknown',
- },
- })
- }
- const handleDeployConfirm = () => {
- sendEvent({
- action: 'edge_function_deploy_updates_confirm_clicked',
- groups: {
- project: ref ?? 'Unknown',
- organization: org?.slug ?? 'Unknown',
- },
- })
- onUpdate()
- }
- useEffect(() => {
- if (initialFiles.length === 0) return
- setFiles(initialFiles)
- }, [initialFiles])
- const hasUnsavedChanges = useMemo(() => {
- const normalizeFiles = (list: FileData[]) =>
- list.map(({ id, name, content }) => ({ id, name, content }))
- return !isEqual(normalizeFiles(initialFiles), normalizeFiles(files))
- }, [initialFiles, files])
- return (
- <div className="flex flex-col h-full">
- {isLoadingFiles && (
- <div className="flex flex-col items-center justify-center h-full bg-surface-200">
- <LogoLoader />
- </div>
- )}
- {isErrorLoadingFiles && (
- <div className="flex flex-col items-center justify-center h-full bg-surface-200">
- <div className="flex flex-col items-center text-center gap-2 max-w-md">
- <AlertCircle size={24} strokeWidth={1.5} className="text-amber-900" />
- <h3 className="text-md mt-4">Failed to load function code</h3>
- <p className="text-sm text-foreground-light">
- {filesError?.message ||
- 'There was an error loading the function code. The format may be invalid or the function may be corrupted.'}
- </p>
- </div>
- </div>
- )}
- {isSuccessLoadingFiles && (
- <>
- <FileExplorerAndEditor
- files={files}
- onFilesChange={(files) => {
- const formattedFiles: FileData[] = files.map((f) => {
- const originalFile = initialFiles.find((x) => x.id === f.id)
- if (!originalFile) {
- return f
- } else if (originalFile.name !== f.name) {
- return { ...f, state: 'new' }
- } else if (originalFile.content !== f.content) {
- return { ...f, state: 'modified' }
- }
- return { ...f, state: 'unchanged' }
- })
- setFiles(formattedFiles)
- }}
- aiEndpoint={`${BASE_PATH}/api/ai/code/complete`}
- aiMetadata={{
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- orgSlug: org?.slug,
- }}
- />
- {IS_PLATFORM && (
- <div className="flex items-center bg-background-muted justify-end p-4 border-t bg-surface-100 shrink-0">
- <ButtonTooltip
- loading={isDeploying}
- size="medium"
- disabled={!canDeployFunction || files.length === 0 || isLoadingFiles}
- onClick={handleDeployClick}
- iconRight={
- isDeploying ? (
- <Loader2 className="animate-spin" size={10} strokeWidth={1.5} />
- ) : (
- <div className="flex items-center space-x-1">
- <CornerDownLeft size={10} strokeWidth={1.5} />
- </div>
- )
- }
- tooltip={{
- content: {
- side: 'top',
- text: !canDeployFunction
- ? 'You need additional permissions to update edge functions'
- : undefined,
- },
- }}
- >
- Deploy updates
- </ButtonTooltip>
- </div>
- )}
- </>
- )}
- <DeployEdgeFunctionWarningModal
- visible={showDeployWarning}
- onCancel={() => setShowDeployWarning(false)}
- onConfirm={handleDeployConfirm}
- isDeploying={isDeploying}
- />
- <PreventNavigationOnUnsavedChanges hasChanges={hasUnsavedChanges} />
- </div>
- )
- }
- CodePage.getLayout = (page: React.ReactNode) => {
- return (
- <DefaultLayout>
- <EdgeFunctionDetailsLayout title="Code">{page}</EdgeFunctionDetailsLayout>
- </DefaultLayout>
- )
- }
- export default CodePage
|